﻿using System;
using System.Collections.Generic;

namespace machines_lib
{
    public delegate void TuringSimulationStepDelegate(TMInput input, TMOutput output);

    /// <summary>
    /// Represents a simulator which can "run" a Turing Machine
    /// </summary>
    public class TuringSimulator
    {
        // Dummy function used to prevent NullReferenceException
        // from being thrown in case no other methods are bound to an event
        private static void DoNothing(TMInput x, TMOutput y) { }

        private Exception VerificationFailedException = new InvalidOperationException("It appears that the machine and/or simulator are in an invalid configuration. Check the ErrorMessages field for more info.");

        private TuringMachine machine;
        private MachineState currentState;
        private List<string> errorMessages;

        /// <summary>
        /// Invoked after calling the TuringMachine's Transition method,
        /// before the TuringSimulator's state is updated
        /// </summary>
        public event TuringSimulationStepDelegate BeforeStep;

        /// <summary>
        /// Invoked immediately after the TuringSimulator's state is updated
        /// </summary>
        public event TuringSimulationStepDelegate AfterStep;

        public TuringSimulator(TuringMachine _machine)
        {
            machine = _machine;
            currentState = machine.InitialState;
            errorMessages = new List<string>(2);

            // Prevent NullReferenceException in case the user does not
            // bind any other methods to BeforeStep and/or AfterStep
            BeforeStep += DoNothing;
            AfterStep += DoNothing;
        }

        /// <summary>
        /// Simulate the operation of the Turing Machine from start to finish by beginning with
        /// a totally blank tape
        /// </summary>
        /// <returns>The final state of the simulation</returns>
        public MachineState RunSimulation()
        {
            return RunSimulation(new TMTape(machine.BlankSymbol));
        }

        public MachineState RunSimulation(IEnumerable<char> tape, int tapeHeadIndex)
        {
            return RunSimulation(new TMTape(machine.BlankSymbol, tape, tapeHeadIndex));
        }

        public MachineState RunSimulation(TMTape tape)
        {
            if (!VerifyMachineIntegrity())
                throw new InvalidOperationException("It appears that the machine and/or simulator are in an invalid configuration. Check the ErrorMessages field for more info.");

            currentState = Machine.InitialState;

            while (!Machine.FinalStates.Contains(currentState))
                DoStep(tape);

            return currentState;
        }

        private bool VerifyMachineIntegrity()
        {
            errorMessages.Clear();

            if (!Machine.StateAlphabet.Contains(Machine.InitialState))
                errorMessages.Add("The machine's state alphabet does not contain its initial state " + Machine.InitialState.Name);

            foreach (MachineState state in Machine.FinalStates)
            {
                if (!Machine.StateAlphabet.Contains(state))
                    errorMessages.Add("The state " + state.Name + " appears in the machine's FinalStates, but not in its StateAlphabet");
                if (state == Machine.InitialState)
                    errorMessages.Add("The machine's initial state " + state.Name + " also appears in the machine's FinalStates");
            }

            if (Machine.NoWriteChar.Equals(Machine.BlankSymbol))
                errorMessages.Add("The machine's blank symbol and no-write character are the same: '" + Machine.BlankSymbol + "'");

            return (errorMessages.Count == 0);
        }

        private void DoStep(TMTape tape)
        {
            TMInput input = new TMInput(currentState, tape.Current);
            TMOutput output = Machine.Transition(input);

            BeforeStep.Invoke(input, output);
            currentState = output.state;
            if (!output.outputChar.Equals(machine.NoWriteChar))
                tape.Current = output.outputChar;

            AfterStep.Invoke(input, output);
        }

        /// <summary>
        /// Manually step through computation. This is MUCH more expensive than
        /// using one of the RunSimulation overloads, as each call to Step will
        /// have to verify the machine's integrity. The BeforeStep and AfterStep
        /// events are never invoked when using this technique to perform the computation.
        /// In the event that the machine is already in a final state, this method will
        /// simply do nothing.
        /// </summary>
        public void Step(TMTape tape)
        {
            if (machine.FinalStates.Contains(currentState))
                return;

            if (!VerifyMachineIntegrity())
                throw new InvalidOperationException("It appears that the machine and/or simulator are in an invalid configuration. Check TuringSimulator.ErrorMessages for more info.");

            TMInput input = new TMInput(currentState, tape.Current);
            TMOutput output = Machine.Transition(input);

            currentState = output.state;
            if (!output.outputChar.Equals(machine.NoWriteChar))
                tape.Current = output.outputChar;
        }
        
        public TuringMachine Machine
        {
            get { return machine; }
            set { machine = value; }
        }

        public List<string> ErrorMessages
        {
            get { return errorMessages; }
        }
    }
}
